本节将详细的介绍了如何使用 Express 开发一个 Web 服务器。

包含从项目创建 => 编写代码&创建服务 => 测试服务 => 线上部署完整流程。

1 准备工作

1.1 初始化项目

新建一个目录 express-demo,进入目录,执行 npm init -y 初始化项目。

同时设置 package.jsontype 字段为 module,以便使用 ESM 语法编写代码 (当然也可以使用构建工具转换语法,这个后面单独介绍)。

① 安装依赖

sh npm i express

② 创建 app.js 文件,编写如下内容

```js import express from 'express'

const PORT = 3000 // 用于设置端口号 const app = express() // 创建一个express应用程序实例

// 创建一个 GET /hello 路由 app.get('/hello', (req, res) => { // 返回一个包含 "Hello World" 的 H1 标题的响应 res.send('

Hello World

') })

// 启动 Express 应用程序,监听在指定的端口上 app.listen(PORT, () => { // 在控制台输出服务器运行信息 console.log(Server is running at http://localhost:${PORT}) }) ```

③ 安装 nodemon,用于监听文件变化,自动重启服务

sh npm i nodemon -D

④ 配置开发启动指令

package.json 加入如下内容。

json { "scripts": { "dev": "nodemon app.js" } }

1.2 启动服务

使用 npm run dev 启动项目。

打开浏览器访问 http://localhost:3000/hello,可以看到如下页面。

注意:如果 localhost 无法访问,可能是 hosts 文件没有配置与 127.0.0.1 的映射,可以使用 127.0.0.1 访问(http://127.0.0.1:3000/hello),或者配置 hosts 文件。

1.3 测试服务

通常后端开发我们都会使用一些客户端工具来调试我们的接口,

这里推荐一个开源的 postcat,非常轻量,无需登录注册也可使用,能满足基本的开发调试。

当然功能更加丰富的可以使用 eolink 或者 Apifox,这里不再过多展开,读者选取一个趁手的就行。

下面使用 postcat 演示 API 测试。

安装好后,打开可看见如下界面,先配置一个本地环境。

在 API 页面发起请求的示例如下。

可以看到这里返回了我们设置的内容。

我们可以再修改一下代码为 (保存后会自动重启立马生效)

js res.send('<h1>Hello Express</h1>')

接下来介绍一下 Express 相关方法的使用,过程中再进一步介绍 postcat 的使用。

2 常用方法介绍

2.1 app.use

用于设置中间件函数来处理请求和响应,Express 会按照设置的顺序依次调用中间件函数。

中间件函数支持接受三个参数:

写一个 app.use 放在之前的 app.get 之前。

``js app.use((req, res, next) => { const { method, path } = req console.log([${method}] ${path}`) next() })

// 省略之前的代码 // app.get('/hello',xxx) ```

在通过服务请求可以查看所有请求的方法和路径被打印到终端里。

2.2 传递参数

支持通过 queryheadersbodyparams 传递参数。

我们继续完善一下刚刚的 app.use 代码。

js app.use((req, res, next) => { const { method, path, query, body, headers } = req console.log(`[${method}] ${path}`) console.log('query:', query) console.log('headers:', headers) console.log('body:', body) next() })

再用工具测试一下。

设置一些传递的参数,

发送请求,结果如下。

可以看到正确的获取了传递的 queryheaders 但是 body 是空的。

这是因为 express 默认不支持解析传递的请求体数据,所以我们需要引入一个中间件来解析这种类型的数据,这里使用 express.json() 这个内置的中间件 (基于 body-parser)。

js const app = express() // 创建一个express应用程序实例 // 支持JSON数据解析 app.use(express.json())

再次发送请求就可以看到获取到了 body 数据。

其中 params 主要指路由中携带的 REST 参数,下面是个示例。

js app.get('/hello/:id', (req, res) => { const { params } = req console.log('params', params) res.json(params) })

请求 GET /hello/123 结果如下。

也可以利用工具提供的 REST 参数,请求路径调整为 GET /hello/{id} (部分工具支持的格式也可能是 GET /hello/:id)。

2.3 不同的请求方法

我们可以通过 app.getapp.postapp.putapp.delete 等方法来设置不同的请求方法,格式如下。

js app.METHOD(PATH, HANDLER)

下面我们单独新建一个文件 routes/method.js 来使用这些方法。

js export default function mountMethodDemo(app) { app.get('/method/get', (req, res) => { res.send('GET request') }) app.post('/method/post', (req, res) => { res.send('POST request') }) app.put('/method/put', (req, res) => { res.send('PUT request') }) app.delete('/method/delete', (req, res) => { res.send('DELETE request') }) }

app.js 中引入。

js import mountMethodDemo from './routes/method.js' // 省略之前的代码 mountMethodDemo(app)

然后就可以通过工具来测试了。

同时还支持一种特殊的 app.all,可以匹配所有的请求方法。

js app.all('/method/all', (req, res) => { const { method } = req res.send(`${method} request`) })

2.4 路由路径

Express 使用 path-to-regexp 来匹配路由路径,

路由路径可以是 字符串字符串模式正则表达式

其中字符串的方式,就是上面的常规写法。

\===字符串模式的方式===

可以使用 ?(存在或者不存在)+(连续一个或多个)*(任意字符0个或多个)() 等特殊字符。

js // 匹配 acd 和 abcd app.get('/ab?cd', (req, res) => { res.send('ab?cd') }) // 匹配 abcd、abbcd、abbbcd等 app.get('/ab+cd', (req, res) => { res.send('ab+cd') }) // 匹配 abcd、abxcd、abRABDOMcd、ab123cd等 app.get('/ab*cd', (req, res) => { res.send('ab*cd') }) // 匹配 /abe 和 /abcde app.get('/ab(cd)?e', (req, res) => { res.send('ab(cd)?e') })

\===正则模式===

可以使用正则表达式来匹配路由。

js // 匹配路径中含有 world 的路径 app.get(/world/, (req, res) => { res.send('hello hello') })

2.5 Router

使用 express.Router 可以创建单独的路由实例,这样非常方便我们模块化开发应用。

Router 实例是完整的中间件和路由系统。

下面我们新建一个文件 routes/router-demo.js 来演示 Router 实例的应用。

```js import express from 'express'

const router = express.Router()

router.get('/router/get', (req, res) => { res.send('GET router request') })

router.post('/router/post', (req, res) => { res.send('POST router request') })

export default router ```

app.js 中引入。

```js import demoRouter from './routes/router-demo.js' // 省略其他代码

// 注册 demoRouter 路由 app.use(demoRouter) // 将 demoRouter 路由注册到 /demo 路径下,路由会自动拼接上 /demo 前缀 app.use('/demo', demoRouter) ```

请求结果如下。

2.6 app.route

app.route() 可以用来创建链式路由,可以避免重复的路由名称。

可以用于创建相同路由名称的不同请求方法,同时可以通过 all 设置所有请求的前置处理逻辑。

js app .route('/route/any') .all((req, res, next) => { console.log('pre all', req.method, req.path) next() }) .get((req, res) => { console.log('get request') res.send('get request') }) .post((req, res) => { console.log('post request') res.send('post request') })

测试结果如下。

2.7 响应数据

express 内置了许多开箱即用的设置响应数据的方法,下面我们介绍一些常用的,

我们新建一个文件 routes/response.js 来存放这一小节的路由代码 (同理在 app.js 中引入即可。

```js // routes/response.js import express from 'express'

const router = express.Router() // 省略后续的路由代码

export default router

// app.js中引入 import responseRouter from './routes/response.js'

app.use(responseRouter) ```

res.json

主要用于发送 JSON 数据。

js router.get('/response/json', (req, res) => { res.json({ name: 'express', type: 'framework' }) })

res.send

可以用于发送任意类型的数据。

```js router.get('/response/send', (req, res) => { // html res.send('

hello express

')

// json // res.send({ // name: 'express', // type: 'framework' // })

// string // res.send('hello express')

// Buffer // res.send(Buffer.from('hello express')) }) ```

res.download

用于下载文件。

```js import path from 'path'

router.get('/response/download', (req, res) => { // 指定文件路径 // res.download('package.json') res.download(path.resolve('./package.json')) }) ```

打开浏览器访问 http://localhost:3000/response/download

即可看见触发了文件下载。

2.8 操作 header

新建一个文件 routes/headers.js 来存放这一小节的路由代码 (同理在 app.js 中引入即可。

① 获取 Request header

直接通过 req.headers 即可获取到请求头。

js router.get('/response/get/header', (req, res) => { res.json(req.headers) })

② 设置 Response header

可以通过 res.set 来设置 response header。

js router.get('/response/set/header', (req, res) => { res.set('Content-Type', 'text/html') res.set('token', '123456') res.send('<h1>hello express</h1>') })

2.9 代码整理

前面的代码中的路由有一部分是写在 app.js 中,导入注册也是在 app.js 中,这样会导致代码相对臃肿,我们可以处理成下面的目录结构。

sh ├── app.js ├── middleware | └── index.js ├── package-lock.json ├── package.json └── routes ├── headers.js ├── index.js ├── method.js ├── response.js └── router-demo.js

将中间件注册和路由注册的逻辑放在 app.js 中,具体实现放在 routes/index.jsmiddleware/index.js 下。

这样 app.js 的代码就变成了这样,看上去就简洁很多了。

```js // app.js import express from 'express' import mountMiddleware from './middleware/index.js' import mountRouters from './routes/index.js'

const PORT = 3000 // 用于设置端口号 const app = express() // 创建一个express应用程序实例

mountMiddleware(app) mountRouters(app)

// 启动 Express 应用程序,监听在指定的端口上 app.listen(PORT, () => { // 在控制台输出服务器运行信息 console.log(Server is running at http://localhost:${PORT}) }) ```

mountMiddleware 函数的实现如下。

```js import express from 'express'

export default function mountMiddleware(app) { app.use(express.json()) // 支持body解析

// 自定义中间件函数 app.use((req, res, next) => { const { method, path, query, body, headers } = req console.log([${method}] ${path}) console.log('query:', query) console.log('headers:', headers) console.log('body:', body) next() }) } ```

mountRouters 函数的实现如下。

```js import headerRouter from './headers.js' import responseRouter from './response.js' import demoRouter from './router-demo.js' import mountMethodDemo from './method.js'

const routers = [headerRouter, responseRouter, demoRouter]

export default function mountRouters(app) { mountMethodDemo(app)

// 注册所有router app.use(routers)

// 将 demoRouter 路由注册到 /demo 路径下,路由会自动拼接上 /demo 前缀 app.use('/demo', demoRouter)

// 一些自定义路由 app.get('/hello/:id', (req, res) => { const { params } = req console.log('params', params) res.json(params) })

// 创建一个 GET /hello 路由 app.get('/hello', (req, res) => { // 返回一个包含 "Hello World" 的 H1 标题的响应 // res.send('

Hello World

') res.send('

Hello Express

') }) } ```

2.10 其它

上面只介绍了一些 express 入门掌握的内容,更多 API 可以阅读官方文档掌握:

3 Restful API 实现

在上一章中我们定义了一些 Restful 风格的 API,我们这里可以利用前面掌握的内容来简单实现一下。

新建 routes/restful.js 文件,编写一下如下路由。

| 方法 | 路径 | 描述 | | ------ | -------------- | -------------------------- | | GET | /api/users | 获取所有用户信息 | | GET | /api/users/:id | 根据用户ID获取用户信息 | | POST | /api/users | 创建新用户,请求体包含新用户的信息 | | PUT | /api/users/:id | 根据用户ID更新用户信息,请求体包含更新后的用户信息 | | DELETE | /api/users/:id | 根据用户ID删除用户信息 |

```js import express from 'express'

const router = express.Router()

// 用于测试的数据 const userList = [ { id: 1, name: '张三' }, { id: 2, name: '李四' }, { id: 3, name: '王五' } ]

router.get('/users', (req, res) => { res.json(userList) })

router.get('/users/:id', (req, res) => { // 根据用户 id 查找用户信息 const user = userList.find((item) => item.id === Number(req.params.id)) res.json(user) })

router.post('/users', (req, res) => { // 创建新用户 const user = { id: userList.length + 1, name: req.body.name // 从请求体中获取用户名 } userList.push(user) res.json(user) })

router.put('/users/:id', (req, res) => { // 根据用户 id 查找用户信息 const user = userList.find((item) => item.id === Number(req.params.id)) // 更新用户名称 user.name = req.body.name // 从请求体中获取新的用户名 res.json(user) })

router.delete('/users/:id', (req, res) => { // 查找要删除的用户在列表中的索引位置 const index = userList.findIndex((item) => item.id === Number(req.params.id)) // 获取要删除的用户信息 const delUser = userList[index] // 从列表中删除该用户 userList.splice(index, 1) res.json({ message: '删除成功', data: delUser }) })

export default router ```

然后在 routes/index.js 中注册路由。

```js import restfulRouter from './restful.js'

app.use('/api', restfulRouter) ```

测试结果如下。

| 方法 | 路径 | 结果 | | ------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | GET | /api/users | | | GET | /api/users/:id | | | POST | /api/users | | | PUT | /api/users/:id | | | DELETE | /api/users/:id | |

4 静态资源目录代理

可以使用 express.static 设置静态资源目录,

这样可以直接访问目标目录下的文件资源。

js app.use(express.static('public'))

访问 http://localhost:3000/ 即可看到效果。

几个文件代码如下。

```html

Document

Hello Express

```

css h1{ color: red; }

js window.onload = () => { document.querySelector('#btn').addEventListener('click', () => { window.alert('Hello World!') }) }

5 文件上传

处理文件上传的第三方库有很多,比如 busboymulterformidable 等等。

这里我们使用 multer 来处理文件上传。

① 安装依赖

sh npm i multer

② 编写文件上传路由 routes/upload.js

js // 引入需要的模块 import express from 'express' // 引入 Express 框架 import multer from 'multer' // 引入 Multer 模块 import fs from 'fs' // 引入 Node.js 文件系统模块 const router = express.Router() // 指定文件存储位置和文件名 const storage = multer.diskStorage({ destination(req, file, cb) { // 这里的 destination() 函数指定了文件存储的目录 const dir = './uploads' // './uploads' 为指定文件存储的目录 if (!fs.existsSync(dir)) { // 如果该目录不存在,则创建该目录 fs.mkdirSync(dir, { recursive: true }) } cb(null, './uploads') // 将文件存储到指定目录 }, filename(req, file, cb) { // 这里的 filename() 函数指定了文件命名规则 const ext = file.originalname.split('.').pop() // 获取文件后缀名 cb(null, `${Date.now()}-${file.fieldname}.${ext}`) // 将文件存储到指定位置,并以指定的文件名命名 } }) // 创建一个 multer 实例并配置相关选项 const upload = multer({ storage, // 存储位置和文件名规则 limits: { fileSize: 1024 * 1024 * 5 // 限制文件大小为 5 MB }, fileFilter(req, file, cb) { // 这里的 fileFilter() 函数指定了文件类型过滤规则 // 拒绝上传非图片类型的文件 if (!file.mimetype.startsWith('image/')) { const err = new Error('Only image files are allowed!') // 错误的具体信息 err.status = 400 // 设置错误状态码为 400 return cb(err, false) } return cb(null, true) } }) // 处理文件上传请求 router.post('/upload/image', upload.single('file'), (req, res) => { // 这里的 upload.single() 函数指定了只上传单个文件 res.json({ message: '文件上传成功', data: req.file }) // 返回上传成功的信息和上传的文件信息 }) export default router // 导出路由模块

③ 新建 public/upload.html 上传文件页面

```html

上传文件示例

上传文件示例

```

④ 访问页面测试 http://localhost:3000/upload.html

测试页面上传。

上传结果如下。

6 操作数据库

要让应用真正的活起来,当然离不开数据库,常用的数据库有 MySQL、MongoDB、Redis 等等。

PS:默认读者已经了解了数据库的基本操作,本小节不会介绍数据库相关的基本知识和安装

关于SQL入门大家可以阅读 廖雪峰: SQL教程

数据库管理工具,笔者使用 VS Code 插件: Database Client,支持操作常见的关系型与非关系型数据库

image.png

在配置 localhost 不生效的场景,可将其改为 127.0.0.1

如何检查&配置 localhost 与 127.0.0.1 映射? * linux/mac:修改`/etc/hosts` 文件配置 * windows:修改 `C:\Windows\System32\drivers\etc\hosts` 文件配置

下面介绍一下如何在 Node.js 中操作数据库。

常用的操作数据库的第三方库有很多,比如 SequelizeMongoosetypeormprisma 等等。

下面是一个近期的下载量对比。

SequelizeMongoose 略多一点,后 2 者比较适合大型项目使用,功能更加丰富。

下面分别介绍一下使用它们如何简单操作 mysql 和 mongodb。

6.1 Sequelize 操作 MySQL

新建一个 node_test 数据库和一张用于测试的 users 表。

```sql create database node_test;

CREATE TABLE IF NOT EXISTS users ( id INT(11) NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age INT(3) NOT NULL, PRIMARY KEY (id) ); ```

创建 db/mysql.js 文件存放本小节代码。

安装依赖。

sh npm i sequelize mysql2

创建 Sequelize 实例。

```js import Sequelize from 'sequelize'

const sequelize = new Sequelize('node_test', 'root', 'password', { host: 'localhost', dialect: 'mysql' }) ```

定义 users 表对应的模型。

js // 定义模型 const User = sequelize.define( 'User', { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false }, name: { type: Sequelize.STRING(50), allowNull: false }, age: { type: Sequelize.INTEGER(3), allowNull: false } }, { tableName: 'users', // 指定表格名称 timestamps: false // 禁止 Sequelize 自动生成 createdAt 和 updatedAt 字段 } )

创建 CRUD 操作方法。

```js // 创建记录 async function createUser(name, age) { const user = await User.create({ name, age }) return user.toJSON() }

// 查询所有记录 async function findAllUsers() { const users = await User.findAll() return users.map((user) => user.toJSON()) }

// 根据 id 查询记录 async function findUserById(id) { const user = await User.findByPk(id) return user?.toJSON() }

// 更新记录 async function updateUser(id, name, age) { const user = await User.findByPk(id) if (user) { user.name = name user.age = age await user.save() console.log(user.toJSON()) } else { console.log('User not found') } return user }

// 删除记录 async function deleteUser(id) { const user = await User.findByPk(id) if (user) { await user.destroy() console.log('User deleted') } else { console.log('User not found') } return user }

export const UserDb = { User, createUser, findAllUsers, findUserById, updateUser, deleteUser } ```

创建一个测试文件 tests/mysql.js 测试上述的方法。

```js import { UserDb } from '../db/mysql.js'

// 测试 async function test() { // 添加3个用户信息 console.log('create', await UserDb.createUser('Alice', 25)) console.log('create', await UserDb.createUser('Tom', 20)) console.log('create', await UserDb.createUser('Lisa', 22))

// 查询所有的用户信息 console.log('findAll', await UserDb.findAllUsers())

// 查询id为1的用户信息 console.log('findById', await UserDb.findUserById(1))

// 更新id为1的用户信息 await UserDb.updateUser(1, 'Alice Smith', 28) // 删除id为2的用户信息 await UserDb.deleteUser(2)

// 查询所有的用户信息 console.log('findAll', await UserDb.findAllUsers()) }

test() ```

| 运行前 | 运行结果 | 运行后 | | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | | | | |

开发中只需要将数据库操作和路由结合就能完成常规的 CRUD 接口开发了。

6.2 Mongoose 操作 MongoDB

① 项目安装依赖

sh npm i mongoose

创建一个 db/mongodb.js 存放本小节代码

② 建立数据库连接

js // 引入mongoose库 import mongoose from 'mongoose' // 连接MongoDB数据库,本地地址为mongodb://localhost:27017/node_test mongoose.connect('mongodb://localhost:27017/node_test')

③ 定义模型

```js // 获取Schema对象 const { Schema } = mongoose

// 定义userSchema,包含id、name和age字段 const userSchema = new Schema({ id: Number, name: String, age: Number }) // 根据userSchema创建用户模型,集合名为users const User = mongoose.model('User', userSchema, 'users') ```

④ 创建 CRUD 操作方法

```js // 创建记录 function createUser(id, name, age) { const newUser = new User({ id, name, age }) // 保存记录并返回Promise实例 return newUser.save() }

// 查询所有记录 function findAllUsers() { // 查找所有用户记录,并返回Promise实例 return User.find({}) } // 根据id查询记录 function findUserById(id) { // 根据id查找用户记录,并返回Promise实例 return User.findOne({ id }) }

// 更新记录 function updateUser(id, name, age) { // 根据id更新用户记录,并返回Promise实例 return User.updateOne({ id }, { name, age }) }

// 删除记录 function deleteUser(id) { // 根据id删除用户记录,并返回Promise实例 return User.deleteOne({ id }) }

// 导出定义的库对象,包括User模型和各种操作方法 export const UserDb = { User, createUser, findAllUsers, findUserById, updateUser, deleteUser } ```

编写一个测试文件 tests/mongodb.js 测试上述的方法。

```js import { UserDb } from '../db/mongodb.js'

// 测试 async function test() { // 添加3个用户信息 console.log('create', await UserDb.createUser(1, 'Alice', 25)) console.log('create', await UserDb.createUser(2, 'Tom', 20)) console.log('create', await UserDb.createUser(3, 'Lisa', 22))

// 查询所有的用户信息 console.log('findAll', await UserDb.findAllUsers())

// 查询id为1的用户信息 console.log('findById', await UserDb.findUserById(1))

// 更新id为1的用户信息 await UserDb.updateUser(1, 'Alice Smith', 28)

// 删除id为2的用户信息 await UserDb.deleteUser(2)

// 查询所有的用户信息 console.log('findAll', await UserDb.findAllUsers()) } test() ```

运行结果

| 运行前 | 运行结果 | 运行后 | | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | | | | |

针对 id mongodb 会自动生成一个私有的字段 _id,用户也可以使用 MongoDB-ObjectId 去生成自己的字段。

下面是 ObjectId 的使用示例。

```js // 引入mongoose库 import mongoose from 'mongoose' // 获取ObjectId对象 const { ObjectId } = mongoose.Types // 创建新的objectId实例 const objectId = new ObjectId()

// 打印objectId实例 console.log(objectId) // 打印objectId实例的字符串格式 console.log(objectId.toString()) // 打印objectId实例的16进制字符串格式 console.log(objectId.toHexString()) // 打印objectId实例的日期时间戳 console.log(objectId.getTimestamp()) ```

7 线上部署服务

开发的时候我们使用了 nodemon 部署的时候,通常需要考虑 日志记录崩溃自动重启应用监控 等问题,

因此我们线上部署就要使用一些工具辅助部署解决这些问题。

常用的部署方式有两种,一种是直接部署到服务器 (如使用 PM2),另一种是使用容器部署 (如 Docker)。

我们主要介绍一下 PM2 的用法。

7.1 PM2 介绍

PM2 是一个守护进程管理器,可以帮助我们管理和监控 Node.js 应用。

通过 npm 即可一键安装。

sh npm i -g pm2

7.2 PM2 启动服务

启动方式有很多种,咱们一个个介绍一下。

① 类似 Node 直接执行的方式

sh pm2 start app.js

查看运行日志。

sh pm2 logs

此时我们访问 http://localhost:3000 即可看到之前设置的页面。

修改文件服务也不会 重启

可以使用 pm2 stop <name> 来停止目标服务。

sh pm2 stop app

运行结果

② 启动时设置服务名称

sh pm2 start app.js --name myapp

运行结果

③ 通过运行指定 npm 指令启动

通常我们会在项目里定义一个 npm start script 用于启动项目。

json "scripts": { "dev": "nodemon app.js", "start": "node app.js" }

可以通过 pm2 start npm 来启动项目。

```sh

pm2 start npm --name -- run start

pm2 start npm --name app2 -- run start ```

7.3 其它常用命令

进一步了解可以阅读文章为什么 Node 应用要用 PM2 来跑?

小结

本节详细的介绍了如何使用 Express 开发一个 Web 服务器。

(从项目创建 => 编写代码&创建服务 => 测试服务 => 线上部署)

过程中介绍了 Express 常见用法和一些重要概念:

实践部分介绍了:

最后详细介绍了如何使用 PM2 对项目进行部署,介绍了多种部署方式。